Перейти к основному содержимому

5.22. Основы языка

Разработчику Архитектору

Основы языка

Dart — это современный язык программирования, созданный компанией Google и предназначенный для разработки высокопроизводительных, масштабируемых и кроссплатформенных приложений. Язык сочетает в себе простоту синтаксиса, строгую типизацию, объектно-ориентированную архитектуру и мощные инструменты для асинхронного программирования. Dart получил широкое распространение благодаря фреймворку Flutter, который позволяет создавать нативные мобильные, десктопные и веб-приложения из единой кодовой базы. Однако сам язык не ограничивается экосистемой Flutter и может использоваться самостоятельно для серверной разработки, скриптов, утилит командной строки и других задач.

История и цели создания

Язык Dart был представлен в 2011 году как альтернатива JavaScript для веб-разработки. Первоначальная цель состояла в том, чтобы предложить более структурированный, предсказуемый и производительный способ написания клиентского кода в браузере. Со временем направление развития сместилось: вместо замены JavaScript Dart стал основой для кроссплатформенной разработки через Flutter, где он проявил свои сильные стороны — компиляцию в машинный код, управление памятью, поддержку реактивного UI и удобную модель работы с асинхронностью.

Dart разрабатывается как open-source проект под лицензией BSD и активно поддерживается сообществом. Он прошёл несколько крупных ревизий, каждая из которых усилила его выразительность, безопасность и производительность. Современные версии Dart (начиная с Dart 2) делают ставку на звуковую типизацию, то есть систему типов, которая помогает избегать ошибок ещё на этапе разработки, а не во время выполнения.

Синтаксис и структура программы

Программа на Dart начинается с точки входа — функции main. Эта функция вызывается автоматически при запуске приложения и служит отправной точкой для выполнения кода. Простейшая программа выглядит так:

void main() {
print('Привет, Вселенная IT!');
}

Ключевое слово void указывает, что функция ничего не возвращает. Функция print выводит текст в консоль. Такой минималистичный пример уже демонстрирует основные черты языка: явное объявление функций, использование фигурных скобок для тела блока, отсутствие необходимости в точке с запятой в конце каждой строки (хотя она допустима), а также чёткую иерархию кода.

Dart следует принципам C-подобного синтаксиса: операторы, условия, циклы и объявления функций используют привычные конструкции, такие как if, for, while, return. Это делает язык доступным для разработчиков, имеющих опыт в Java, C#, JavaScript или TypeScript.

Типы данных и переменные

Dart — язык со статической типизацией, но он также поддерживает вывод типов. Это означает, что разработчик может явно указывать тип переменной, а может позволить компилятору определить его автоматически на основе значения.

Переменные объявляются с помощью ключевых слов var, final или const, а также с указанием конкретного типа:

  • var name = 'Dart'; — компилятор выводит, что name имеет тип String.
  • String language = 'Dart'; — тип указан явно.
  • final version = '3.0'; — переменная инициализируется один раз и не может быть изменена позже.
  • const pi = 3.14159; — константа, значение которой известно на этапе компиляции и неизменно в течение всего времени выполнения.

Основные встроенные типы в Dart включают:

  • int — целые числа, например, 42.
  • double — числа с плавающей точкой, например, 3.14.
  • bool — логический тип со значениями true и false.
  • String — последовательности символов в одинарных или двойных кавычках, поддерживающие интерполяцию: 'Значение: $pi'.
  • List — упорядоченная коллекция элементов, например, [1, 2, 3].
  • Set — неупорядоченная коллекция уникальных элементов.
  • Map — ассоциативный массив, хранящий пары «ключ — значение».

Все эти типы являются объектами, даже числа и логические значения. Это означает, что у любого значения можно вызывать методы и обращаться к свойствам. Например, строка 'привет'.toUpperCase() возвращает 'ПРИВЕТ'.

Функции

Функции в Dart — это полноправные объекты первого класса. Их можно присваивать переменным, передавать как аргументы другим функциям и возвращать из функций. Объявление функции включает тип возвращаемого значения, имя, список параметров и тело.

Пример функции с явным возвратом:

int add(int a, int b) {
return a + b;
}

Dart поддерживает сокращённый синтаксис для функций, состоящих из одного выражения:

int multiply(int a, int b) => a * b;

Стрелочная нотация (=>) эквивалентна записи с return и фигурными скобками, но короче и читабельнее для простых операций.

Функции могут иметь именованные параметры, которые передаются по ключу, а не по позиции. Это повышает читаемость вызовов:

void greet({required String name, String? greeting}) {
print('${greeting ?? 'Привет'}, $name!');
}

Здесь name — обязательный именованный параметр, а greeting — необязательный, который может быть null. Оператор ?? предоставляет значение по умолчанию, если переменная равна null.

Классы и объектно-ориентированное программирование

Dart — полностью объектно-ориентированный язык. Все значения являются экземплярами классов, и каждый класс наследуется от корневого класса Object.

Класс объявляется с помощью ключевого слова class. Он может содержать поля, методы, конструкторы и геттеры/сеттеры:

class Point {
double x;
double y;

Point(this.x, this.y);

double distanceToOrigin() {
return (x * x + y * y).sqrt();
}
}

Конструктор Point(this.x, this.y) использует синтаксический сахар Dart — автоматическое присвоение параметров полям. Метод distanceToOrigin вычисляет расстояние от точки до начала координат, используя метод sqrt() из стандартной библиотеки.

Dart поддерживает наследование, абстрактные классы, интерфейсы и миксины. Интерфейсы реализуются неявно: любой класс автоматически определяет интерфейс, содержащий все его публичные методы и свойства. Это позволяет легко применять полиморфизм без необходимости явного объявления interface.

Миксины — это способ повторного использования кода в нескольких иерархиях наследования. Они позволяют добавлять функциональность классу без создания жёсткой связи через наследование:

mixin Flyable {
void fly() => print('Лечу!');
}

class Bird with Flyable {}

Экземпляр Bird теперь обладает методом fly, несмотря на то, что Flyable не является классом-родителем.


Асинхронное программирование

Одной из ключевых особенностей Dart является встроенная поддержка асинхронного выполнения кода. Язык предоставляет элегантные и читаемые механизмы для работы с операциями, которые не завершаются мгновенно: сетевыми запросами, чтением файлов, взаимодействием с базами данных или ожиданием пользовательского ввода. Вместо блокировки основного потока выполнения, Dart использует модель событий и управление через объекты Future и ключевые слова async / await.

Объект Future представляет собой обещание получить значение в будущем. Он может находиться в одном из трёх состояний: незавершённом, успешно завершённом или завершённом с ошибкой. Функция, возвращающая Future, помечается как async. Внутри такой функции можно использовать await, чтобы дождаться результата асинхронной операции, не прерывая читаемость кода:

Future<String> fetchUserData() async {
var response = await httpClient.get('https://api.example.com/user');
return response.body;
}

Этот подход позволяет писать асинхронный код так, будто он синхронный, сохраняя линейную структуру и избегая «ада колбэков». Dart также поддерживает потоки данных через класс Stream, который используется для обработки последовательностей событий во времени — например, нажатий кнопок, обновлений сенсоров или входящих сообщений WebSocket. Потоки могут быть прослушаны с помощью async* и yield, что даёт возможность создавать генераторы событий.

Обработка ошибок

Dart использует механизм исключений, аналогичный другим современным языкам. Ошибки возникают с помощью ключевого слова throw, перехватываются конструкцией try-catch, а необязательный блок finally выполняется в любом случае — успешно завершился код или произошла ошибка. Исключения в Dart не требуют объявления в сигнатуре функции, что упрощает проектирование API.

try {
var result = await riskyOperation();
print('Успех: $result');
} on NetworkException catch (e) {
print('Ошибка сети: ${e.message}');
} catch (e) {
print('Неизвестная ошибка: $e');
} finally {
cleanupResources();
}

Такой подход обеспечивает гибкость и предсказуемость при работе с ненадёжными операциями.

Коллекции и работа с данными

Dart предоставляет богатый набор встроенных коллекций: списки (List), множества (Set) и словари (Map). Все они поддерживают литералы, методы высшего порядка и функциональные преобразования. Например, список чисел можно фильтровать, преобразовывать и сворачивать одной цепочкой вызовов:

var numbers = [1, 2, 3, 4, 5];
var doubledEvens = numbers
.where((n) => n.isEven)
.map((n) => n * 2)
.toList();

Результатом будет [4, 8]. Такие операции делают обработку данных выразительной и лаконичной. Dart также поддерживает расширения коллекций через spread-оператор (...) и условные элементы (if внутри литерала), что особенно полезно при построении UI в Flutter.

Null safety

Начиная с версии 2.12, Dart включает систему звуковой null safety — механизм, который гарантирует отсутствие ошибок, связанных с обращением к null. Каждый тип в Dart теперь либо допускает значение null, либо нет. Тип String не может быть null, а String? — может. Компилятор проверяет все возможные пути выполнения и требует явной обработки случаев, когда значение может отсутствовать.

Это достигается за счёт трёх основных принципов:

  • Non-nullable by default: переменные без знака вопроса не могут содержать null.
  • Flow analysis: компилятор отслеживает, где значение уже проверено на null, и разрешает его использование.
  • Required initialisation: все поля класса должны быть инициализированы до того, как объект станет доступен.

Null safety резко снижает количество runtime-ошибок и делает код более надёжным без необходимости вручную проверять каждую переменную.

Изоляты и конкурентность

Dart однопоточный по своей природе, но поддерживает конкурентность через изоляты — независимые рабочие единицы, которые не разделяют память. Каждый изолят имеет собственную память и очередь событий, а обмен данными между ними происходит только через передачу сообщений. Это исключает гонки данных и упрощает написание параллельного кода.

Изоляты особенно полезны для выполнения тяжёлых вычислений, которые могут заблокировать основной поток UI. Например, в мобильном приложении на Flutter фоновый изолят может обрабатывать изображение, не замедляя интерфейс.

Инструментарий и экосистема

Dart поставляется с мощным набором инструментов: компилятором, виртуальной машиной (Dart VM), пакетным менеджером pub и интеграцией с популярными IDE. Пакеты Dart публикуются в реестре pub.dev, где доступны тысячи библиотек — от утилит для работы с датами до полноценных фреймворков.

Компиляция Dart возможна в несколько целей:

  • JIT (Just-In-Time) — для быстрой разработки с горячей перезагрузкой.
  • AOT (Ahead-Of-Time) — для выпуска нативных приложений с высокой производительностью.
  • JavaScript — для запуска в браузере.

Эта гибкость делает Dart универсальным языком, подходящим как для клиентской, так и для серверной разработки.


Модульность и организация кода

Dart поощряет чёткое разделение кода на логические единицы — библиотеки. Каждый файл Dart автоматически является библиотекой, даже если в нём не указано явное объявление library. Это позволяет легко импортировать функциональность из других файлов с помощью директивы import. Например, чтобы использовать математические функции или коллекции, достаточно написать:

import 'dart:math';
import 'dart:collection';

Стандартная библиотека Dart разделена на тематические модули: dart:core содержит базовые типы и функции, dart:async — инструменты для асинхронности, dart:io — операции ввода-вывода, dart:convert — кодирование и декодирование данных. Все эти модули доступны без установки дополнительных пакетов.

Помимо стандартных библиотек, Dart поддерживает импорт сторонних пакетов через pubspec.yaml — файл конфигурации проекта. После добавления зависимости и выполнения команды dart pub get, любой компонент пакета становится доступен для импорта:

import 'package:http/http.dart' as http;

Ключевое слово as создаёт псевдоним, что помогает избежать конфликтов имён. Также можно использовать show и hide, чтобы импортировать только нужные части библиотеки или скрыть нежелательные:

import 'utils.dart' show formatDate, validateEmail;

Такой подход повышает читаемость и уменьшает объём загружаемого кода.

Генерики и параметризованные типы

Dart поддерживает обобщённое программирование через генерики. Это позволяет создавать классы, интерфейсы и функции, которые работают с любыми типами, сохраняя при этом безопасность типов. Например, список целых чисел объявляется как List<int>, а словарь строковых ключей и значений — как Map<String, String>.

Генерики особенно полезны при создании переиспользуемых структур данных. Рассмотрим простой контейнер:

class Box<T> {
T value;
Box(this.value);

T getValue() => value;
}

Здесь T — параметр типа. При создании экземпляра указывается конкретный тип:

var stringBox = Box<String>('Привет');
var numberBox = Box<int>(42);

Компилятор гарантирует, что в stringBox нельзя случайно поместить число, а метод getValue() всегда вернёт значение ожидаемого типа. Это устраняет необходимость в приведении типов и предотвращает ошибки на этапе выполнения.

Метапрограммирование и аннотации

Dart предоставляет ограниченные, но эффективные средства метапрограммирования через аннотации и рефлексию. Аннотации — это метаданные, которые можно применять к классам, методам, переменным и другим элементам кода. Они не влияют на выполнение программы напрямую, но могут использоваться инструментами во время сборки или анализа.

Например, аннотация @deprecated помечает устаревший API:

@deprecated
void oldMethod() {
// ...
}

Современный подход к метапрограммированию в Dart — это code generation. Специальные генераторы, запускаемые через build_runner, анализируют исходный код и создают дополнительные файлы с шаблонным кодом: сериализаторы, мапперы, адаптеры. Это позволяет избежать ручного написания повторяющейся логики и сохранить производительность, так как всё генерируется на этапе компиляции, а не во время выполнения.

Стандартная библиотека и её возможности

Стандартная библиотека Dart — это фундамент, на котором строятся все приложения. Она включает в себя не только базовые типы, но и мощные утилиты для работы с датами, регулярными выражениями, URI, JSON, файловой системой и сетевыми соединениями. Все эти компоненты тщательно оптимизированы и документированы.

Работа с JSON, например, осуществляется через функции jsonEncode и jsonDecode из dart:convert. Хотя Dart не имеет встроенной поддержки автоматической сериализации объектов, паттерны преобразования легко реализуются вручную или с помощью генераторов кода.

Для работы с датами и временем используется класс DateTime, который поддерживает создание, сравнение, форматирование и арифметические операции. Регулярные выражения представлены классом RegExp, совместимым с большинством современных движков.

Особенности Dart в контексте Flutter

Хотя Dart — самостоятельный язык, его наибольшее распространение получило в связке с Flutter. В этом контексте многие особенности языка раскрываются особенно ярко. Декларативный стиль UI, реактивность, горячая перезагрузка, эффективное управление состоянием — всё это опирается на возможности Dart: быструю компиляцию, null safety, асинхронность, генерики и работу с коллекциями.

Например, виджеты в Flutter — это обычные классы Dart. Их дерево строится с помощью конструкторов и литералов, а изменения состояния вызывают пересоздание частей дерева. Благодаря эффективному diff-алгоритму и компиляции в нативный код, такие операции выполняются мгновенно.